<!DOCTYPE>
<!--解决idea thymeleaf 表达式模板报红波浪线-->
<!--suppress ALL -->
<html xmlns:th="http://www.thymeleaf.org" xmlns="http://www.w3.org/1999/html">
<head>
    <meta charset="UTF-8">
    <title>WebRTC + WebSocket</title>
    <meta name="viewport" content="width=device-width,initial-scale=1.0,user-scalable=no">
    <style>
        html, body {
            margin: 0;
            padding: 0;
        }

        #main {
            position: absolute;
            width: 370px;
            height: 550px;
        }

        #localVideo {
            position: absolute;
            background: #757474;
            top: 10px;
            right: 10px;
            width: 100px;
            height: 150px;
            z-index: 2;
        }

        #remoteVideo {
            position: absolute;
            top: 0px;
            left: 0px;
            width: 100%;
            height: 100%;
            background: #222;
        }

        #buttons {
            z-index: 3;
            bottom: 20px;
            left: 90px;
            position: absolute;
        }

        #toUser {
            border: 1px solid #ccc;
            padding: 7px 0px;
            border-radius: 5px;
            padding-left: 5px;
            margin-bottom: 5px;
        }

        #toUser:focus {
            border-color: #66afe9;
            outline: 0;
            -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 8px rgba(102, 175, 233, .6);
            box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 8px rgba(102, 175, 233, .6)
        }

        #call {
            width: 70px;
            height: 35px;
            background-color: #00BB00;
            border: none;
            margin-right: 25px;
            color: white;
            border-radius: 5px;
        }

        #hangup {
            width: 70px;
            height: 35px;
            background-color: #FF5151;
            border: none;
            color: white;
            border-radius: 5px;
        }
    </style>
    <script src="/js/jquery.js"></script>
    <script src="/js/sockjs.min.js"></script>
    <script src="/js/stomp.min.js"></script>
</head>
<body>
<div id="main">
    <video id="remoteVideo" playsinline autoplay></video>
    <video id="localVideo" playsinline autoplay muted></video>

    <div id="buttons">
        <input id="fromUser" placeholder="输入登录账号"/><br/>
        <button id="login">登录</button>
        <br/>
        <input id="toUser" placeholder="输入在线好友账号"/><br/>
        <button id="call">视频通话</button>
        <button id="hangup">挂断</button>
    </div>
</div>
</body>
<script type="text/javascript" th:inline="javascript">
    let username = [[${uname}]];
    let localVideo = document.getElementById('localVideo');
    let remoteVideo = document.getElementById('remoteVideo');
    let peer = null;

    $('#login').click(function (e) {
        // 登录成功
        username = $('#fromUser').val();
        $.ajax('http://localhost:8080/login?name=' + username, {
            method: 'GET',
            success: function (result) {
                console.log("登录成功", result);
                init();
            },
            error: function (err) {
                console.log(err);
            }
        })
    });
    if (username) {
        init();
    }

    function init() {
        $('#login').hide();
        WebSocketInit();
        ButtonFunInit();
    }

    /* WebSocket */
    var stompClient;

    function WebSocketInit() {
        // 创建websocket连接，并订阅消息事件
        const socket = new SockJS("/video/" + username);
        stompClient = Stomp.over(socket);
        stompClient.connect({"uname": username}, function (frame) {
            console.log('Connected: ' + frame);
            stompClient.subscribe('/user/topic/video', async function (greeting) {
                    // 表示这个长连接，订阅了 "/topic/hello" , 这样后端像这个路径转发消息时，我们就可以拿到对应的返回
                    console.log("resp: ", greeting.body)
                    await subAck(greeting.body);
                }, {id: username}
            )
        }, {id: username});
    }

    async function subAck(data) {
        // 处理返回的信息
        if (!data.startsWith("{")) {
            return;
        }

        let {
            type,
            fromUser,
            msg,
            sdp,
            iceCandidate
        } = JSON.parse(data.replace(/\n/g, "\\n").replace(/\r/g, "\\r"));

        console.log(type);

        if (type === 'hangup') {
            // 对方挂断了
            console.log(msg);
            document.getElementById('hangup').click();
            return;
        }

        // fromUser 发起通话请求，当前用户 username 选择是否接受
        if (type === 'call_start') {
            let msg = "0"
            if (confirm(fromUser + "发起视频通话，确定接听吗") == true) {
                document.getElementById('toUser').value = fromUser;
                WebRTCInit();
                msg = "1"
            }

            send(fromUser, JSON.stringify({
                type: "call_back",
                toUser: fromUser,
                fromUser: username,
                msg: msg
            }));
            return;
        }

        // 发起通话请求的用户 username，接受对方 toUser 的响应结果，msg = 1表示对方接收通话 msg == 0 表示对方拒绝通话连接
        if (type === 'call_back') {
            if (msg === "1") {
                console.log(document.getElementById('toUser').value + "同意视频通话");

                //创建本地视频并发送offer
                let stream = await navigator.mediaDevices.getUserMedia({video: true, audio: true})
                localVideo.srcObject = stream;
                stream.getTracks().forEach(track => {
                    peer.addTrack(track, stream);
                });

                let offer = await peer.createOffer();
                await peer.setLocalDescription(offer);

                let newOffer = offer.toJSON();
                newOffer["fromUser"] = username;
                newOffer["toUser"] = document.getElementById('toUser').value;
                send(newOffer["toUser"], JSON.stringify(newOffer));
            } else if (msg === "0") {
                alert(document.getElementById('toUser').value + "拒绝视频通话");
                document.getElementById('hangup').click();
            } else {
                alert(msg);
                document.getElementById('hangup').click();
            }

            return;
        }

        if (type === 'offer') {
            let stream = await navigator.mediaDevices.getUserMedia({video: true, audio: true});
            localVideo.srcObject = stream;
            stream.getTracks().forEach(track => {
                peer.addTrack(track, stream);
            });

            await peer.setRemoteDescription(new RTCSessionDescription({type, sdp}));
            let answer = await peer.createAnswer();
            let newAnswer = answer.toJSON();

            newAnswer["fromUser"] = username;
            newAnswer["toUser"] = document.getElementById('toUser').value;
            send(newAnswer["toUser"], JSON.stringify(newAnswer));
            await peer.setLocalDescription(answer);
            return;
        }

        if (type === 'answer') {
            peer.setRemoteDescription(new RTCSessionDescription({type, sdp}));
            return;
        }

        if (type === '_ice') {
            peer.addIceCandidate(iceCandidate);
            return;
        }
    }

    /* WebRTC */
    function WebRTCInit() {
        peer = new RTCPeerConnection();

        /*
        ICE（Interactive Connectivity Establishment），是一种用于实现网络连接的技术框架，用于在对等连接（如实时通信、P2P 文件共享等）中解决 NAT（Network Address Translation）和防火墙等网络障碍的问题。 ICE 是一种框架，可以通过使用多种技术（如 STUN、TURN、NAT 透明性检测等）来搜索可用的网络路径，并选择最优的路径建立连接，从而解决了 NAT 和防火墙等网络障碍的问题。 ICE 框架包含了以下几个步骤：
            收集网络接口信息，包括本地 IP 地址、端口等；
            通过 STUN 服务器获取公网 IP 地址和端口号；
            通过 NAT 透明性检测来确定 NAT 类型和行为；
            尝试直接连接对等端点；
            如果直接连接失败，则使用 TURN 服务器作为中继节点进行连接。 也就是，ICE 更好的进行 NAT 穿越效果，从而提高实时通信的质量和效率。
         */
        peer.onicecandidate = function (e) {
            if (e.candidate) {
                send(document.getElementById('toUser').value, JSON.stringify({
                    type: '_ice',
                    toUser: document.getElementById('toUser').value,
                    fromUser: username,
                    iceCandidate: e.candidate
                }));
            }
        };

        // 建立成功后，双方就可以通过 onTrack 获取数据并渲染到页面上
        peer.ontrack = function (e) {
            if (e && e.streams) {
                remoteVideo.srcObject = e.streams[0];
            }
        };
    }

    /* 按钮事件 */
    function ButtonFunInit() {
        //视频通话
        document.getElementById('call').onclick = function (e) {
            document.getElementById('toUser').style.visibility = 'hidden';

            let toUser = document.getElementById('toUser').value;
            if (!toUser) {
                alert("请先指定好友账号，再发起视频通话！");
                return;
            }

            if (peer == null) {
                WebRTCInit();
            }

            send(toUser, JSON.stringify({
                type: "call_start",
                fromUser: username,
                toUser: toUser,
            }));
        }

        //挂断
        document.getElementById('hangup').onclick = function (e) {
            document.getElementById('toUser').style.visibility = 'unset';

            if (localVideo.srcObject) {
                const videoTracks = localVideo.srcObject.getVideoTracks();
                videoTracks.forEach(videoTrack => {
                    videoTrack.stop();
                    localVideo.srcObject.removeTrack(videoTrack);
                });
            }

            if (remoteVideo.srcObject) {
                const videoTracks = remoteVideo.srcObject.getVideoTracks();
                videoTracks.forEach(videoTrack => {
                    videoTrack.stop();
                    remoteVideo.srcObject.removeTrack(videoTrack);
                });

                //挂断同时，通知对方
                send(document.getElementById('toUser').value, JSON.stringify({
                    type: "hangup",
                    fromUser: username,
                    toUser: document.getElementById('toUser').value,
                }));
            }

            if (peer) {
                peer.ontrack = null;
                peer.onremovetrack = null;
                peer.onremovestream = null;
                peer.onicecandidate = null;
                peer.oniceconnectionstatechange = null;
                peer.onsignalingstatechange = null;
                peer.onicegatheringstatechange = null;
                peer.onnegotiationneeded = null;

                peer.close();
                peer = null;
            }

            localVideo.srcObject = null;
            remoteVideo.srcObject = null;
        }
    }

    function send(target, content) {
        stompClient.send('/app/video/' + target, {}, content);
    }
</script>
</html>